﻿
namespace GMap.NET.Internals
{
   using System;
   using System.Collections.Generic;
   using System.Diagnostics;
   using System.Threading;
   using GMap.NET.Projections;
   using System.IO;
   using GMap.NET.MapProviders;
   using System.ComponentModel;

#if NET40
    using System.Collections.Concurrent;
    using System.Threading.Tasks;
#endif

#if PocketPC
   using OpenNETCF.ComponentModel;
   using OpenNETCF.Threading;
   using Thread=OpenNETCF.Threading.Thread2;
#endif

   /// <summary>
   /// internal map control core
   /// </summary>
   internal class Core : IDisposable
   {
      public PointLatLng position;
      public GPoint positionPixel;

      public GPoint renderOffset;
      public GPoint centerTileXYLocation;
      public GPoint centerTileXYLocationLast;
      public GPoint dragPoint;
      public GPoint compensationOffset;

      public GPoint mouseDown;
      public GPoint mouseCurrent;
      public GPoint mouseLastZoom;

      public MouseWheelZoomType MouseWheelZoomType = MouseWheelZoomType.MousePositionAndCenter;
      public bool MouseWheelZoomEnabled = true;

      public PointLatLng? LastLocationInBounds = null;
      public bool VirtualSizeEnabled = false;

      public GSize sizeOfMapArea;
      public GSize minOfTiles;
      public GSize maxOfTiles;

      public GRect tileRect;
      public GRect tileRectBearing;
      //public GRect currentRegion;
      public float bearing = 0;
      public bool IsRotated = false;

      public bool fillEmptyTiles = true;

      public TileMatrix Matrix = new TileMatrix();

      public List<DrawTile> tileDrawingList = new List<DrawTile>();
      public FastReaderWriterLock tileDrawingListLock = new FastReaderWriterLock();

#if !NET40
      public readonly Stack<LoadTask> tileLoadQueue = new Stack<LoadTask>();
#endif

#if !PocketPC
      static readonly int GThreadPoolSize = 4;
#else
        static readonly int GThreadPoolSize = 2;
#endif

      DateTime LastTileLoadStart = DateTime.Now;
      DateTime LastTileLoadEnd = DateTime.Now;
      internal volatile bool IsStarted = false;
      int zoom;

      internal double scaleX = 1;
      internal double scaleY = 1;

      internal int maxZoom = 2;
      internal int minZoom = 2;
      internal int Width;
      internal int Height;

      internal int pxRes100m;  // 100 meters
      internal int pxRes1000m;  // 1km  
      internal int pxRes10km; // 10km
      internal int pxRes100km; // 100km
      internal int pxRes1000km; // 1000km
      internal int pxRes5000km; // 5000km

      /// <summary>
      /// is user dragging map
      /// </summary>
      public bool IsDragging = false;

      public Core()
      {
         Provider = EmptyProvider.Instance;
      }

      /// <summary>
      /// map zoom
      /// </summary>
      public int Zoom
      {
         get
         {
            return zoom;
         }
         set
         {
            if (zoom != value && !IsDragging)
            {
               zoom = value;

               minOfTiles = Provider.Projection.GetTileMatrixMinXY(value);
               maxOfTiles = Provider.Projection.GetTileMatrixMaxXY(value);

               positionPixel = Provider.Projection.FromLatLngToPixel(Position, value);

               if (IsStarted)
               {
                  CancelAsyncTasks();

                  Matrix.ClearLevelsBelove(zoom - LevelsKeepInMemmory);
                  Matrix.ClearLevelsAbove(zoom + LevelsKeepInMemmory);

                  lock (FailedLoads)
                  {
                     FailedLoads.Clear();
                     RaiseEmptyTileError = true;
                  }

                  GoToCurrentPositionOnZoom();
                  UpdateBounds();

                  if (OnMapZoomChanged != null)
                  {
                     OnMapZoomChanged();
                  }
               }
            }
         }
      }

      /// <summary>
      /// current marker position in pixel coordinates
      /// </summary>
      public GPoint PositionPixel
      {
         get
         {
            return positionPixel;
         }
      }

      /// <summary>
      /// current marker position
      /// </summary>
      public PointLatLng Position
      {
         get
         {

            return position;
         }
         set
         {
            position = value;
            positionPixel = Provider.Projection.FromLatLngToPixel(value, Zoom);

            if (IsStarted)
            {
               if (!IsDragging)
               {
                  GoToCurrentPosition();
               }

               if (OnCurrentPositionChanged != null)
                  OnCurrentPositionChanged(position);
            }
         }
      }

      public GMapProvider provider;
      public GMapProvider Provider
      {
         get
         {
            return provider;
         }
         set
         {
            if (provider == null || !provider.Equals(value))
            {
               bool diffProjection = (provider == null || provider.Projection != value.Projection);

               provider = value;

               if (!provider.IsInitialized)
               {
                  provider.IsInitialized = true;
                  provider.OnInitialized();
               }

               if (provider.Projection != null && diffProjection)
               {
                  tileRect = new GRect(GPoint.Empty, Provider.Projection.TileSize);
                  tileRectBearing = tileRect;
                  if (IsRotated)
                  {
                     tileRectBearing.Inflate(1, 1);
                  }

                  minOfTiles = Provider.Projection.GetTileMatrixMinXY(Zoom);
                  maxOfTiles = Provider.Projection.GetTileMatrixMaxXY(Zoom);
                  positionPixel = Provider.Projection.FromLatLngToPixel(Position, Zoom);
               }

               if (IsStarted)
               {
                  CancelAsyncTasks();
                  if (diffProjection)
                  {
                     OnMapSizeChanged(Width, Height);
                  }
                  ReloadMap();

                  if (minZoom < provider.MinZoom)
                  {
                     minZoom = provider.MinZoom;
                  }

                  //if(provider.MaxZoom.HasValue && maxZoom > provider.MaxZoom)
                  //{
                  //   maxZoom = provider.MaxZoom.Value;
                  //}

                  zoomToArea = true;

                  if (provider.Area.HasValue && !provider.Area.Value.Contains(Position))
                  {
                     SetZoomToFitRect(provider.Area.Value);
                     zoomToArea = false;
                  }

                  if (OnMapTypeChanged != null)
                  {
                     OnMapTypeChanged(value);
                  }
               }
            }
         }
      }

      internal bool zoomToArea = true;

      /// <summary>
      /// sets zoom to max to fit rect
      /// </summary>
      /// <param name="rect"></param>
      /// <returns></returns>
      public bool SetZoomToFitRect(RectLatLng rect)
      {
         int mmaxZoom = GetMaxZoomToFitRect(rect);
         if (mmaxZoom > 0)
         {
            PointLatLng center = new PointLatLng(rect.Lat - (rect.HeightLat / 2), rect.Lng + (rect.WidthLng / 2));
            Position = center;

            if (mmaxZoom > maxZoom)
            {
               mmaxZoom = maxZoom;
            }

            if (Zoom != mmaxZoom)
            {
               Zoom = (int)mmaxZoom;
            }

            return true;
         }
         return false;
      }

      /// <summary>
      /// is polygons enabled
      /// </summary>
      public bool PolygonsEnabled = true;

      /// <summary>
      /// is routes enabled
      /// </summary>
      public bool RoutesEnabled = true;

      /// <summary>
      /// is markers enabled
      /// </summary>
      public bool MarkersEnabled = true;

      /// <summary>
      /// 是否允许画圆
      /// </summary>
      public bool CirclesEnabled = true;

      /// <summary>
      /// 是否允许画包络线
      /// </summary>
      public bool EnvelopesEnabled = true;

      /// <summary>
      /// can user drag map
      /// </summary>
      public bool CanDragMap = true;

      /// <summary>
      /// retry count to get tile 
      /// </summary>
#if !PocketPC
      public int RetryLoadTile = 0;
#else
      public int RetryLoadTile = 1;
#endif

      /// <summary>
      /// how many levels of tiles are staying decompresed in memory
      /// </summary>
#if !PocketPC
      public int LevelsKeepInMemmory = 5;
#else
      public int LevelsKeepInMemmory = 1;
#endif

      /// <summary>
      /// map render mode
      /// </summary>
      public RenderMode RenderMode = RenderMode.GDI_PLUS;

      /// <summary>
      /// occurs when current position is changed
      /// </summary>
      public event PositionChanged OnCurrentPositionChanged;

      /// <summary>
      /// occurs when tile set load is complete
      /// </summary>
      public event TileLoadComplete OnTileLoadComplete;

      /// <summary>
      /// occurs when tile set is starting to load
      /// </summary>
      public event TileLoadStart OnTileLoadStart;

      /// <summary>
      /// occurs on empty tile displayed
      /// </summary>
      public event EmptyTileError OnEmptyTileError;

      /// <summary>
      /// occurs on map drag
      /// </summary>
      public event MapDrag OnMapDrag;

      /// <summary>
      /// occurs on map zoom changed
      /// </summary>
      public event MapZoomChanged OnMapZoomChanged;

      /// <summary>
      /// occurs on map type changed
      /// </summary>
      public event MapTypeChanged OnMapTypeChanged;

      readonly List<Thread> GThreadPool = new List<Thread>();
      // ^
      // should be only one pool for multiply controls, any ideas how to fix?
      //static readonly List<Thread> GThreadPool = new List<Thread>();

      // windows forms or wpf
      internal string SystemType;

      internal static int instances = 0;

      BackgroundWorker invalidator;

      public BackgroundWorker OnMapOpen()
      {
         if (!IsStarted)
         {
            int x = Interlocked.Increment(ref instances);
            Debug.WriteLine("OnMapOpen: " + x);

            IsStarted = true;

            if (x == 1)
            {
               GMaps.Instance.noMapInstances = false;
            }

            GoToCurrentPosition();

            invalidator = new BackgroundWorker();
            invalidator.WorkerSupportsCancellation = true;
            invalidator.WorkerReportsProgress = true;
            invalidator.DoWork += new DoWorkEventHandler(invalidatorWatch);
            invalidator.RunWorkerAsync();

            //if(x == 1)
            //{
            // first control shown
            //}
         }
         return invalidator;
      }

      public void OnMapClose()
      {
         Dispose();
      }

      internal readonly object invalidationLock = new object();
      internal DateTime lastInvalidation = DateTime.Now;

      void invalidatorWatch(object sender, DoWorkEventArgs e)
      {
         var w = sender as BackgroundWorker;

         TimeSpan span = TimeSpan.FromMilliseconds(111);
         int spanMs = (int)span.TotalMilliseconds;
         bool skiped = false;
         TimeSpan delta;
         DateTime now = DateTime.Now;

         while (Refresh != null && (!skiped && Refresh.WaitOne() || (Refresh.WaitOne(spanMs, false) || true)))
         {
            if (w.CancellationPending)
               break;

            now = DateTime.Now;
            lock (invalidationLock)
            {
               delta = now - lastInvalidation;
            }

            if (delta > span)
            {
               lock (invalidationLock)
               {
                  lastInvalidation = now;
               }
               skiped = false;

               w.ReportProgress(1);
               Debug.WriteLine("Invalidate delta: " + (int)delta.TotalMilliseconds + "ms");
            }
            else
            {
               skiped = true;
            }
         }
      }

      public void UpdateCenterTileXYLocation()
      {
         PointLatLng center = FromLocalToLatLng(Width / 2, Height / 2);
         GPoint centerPixel = Provider.Projection.FromLatLngToPixel(center, Zoom);
         centerTileXYLocation = Provider.Projection.FromPixelToTileXY(centerPixel);
      }

      public int vWidth = 800;
      public int vHeight = 400;

      public void OnMapSizeChanged(int width, int height)
      {
         this.Width = width;
         this.Height = height;

         if (IsRotated)
         {
#if !PocketPC
            int diag = (int)Math.Round(Math.Sqrt(Width * Width + Height * Height) / Provider.Projection.TileSize.Width, MidpointRounding.AwayFromZero);
#else
            int diag = (int) Math.Round(Math.Sqrt(Width * Width + Height * Height) / Provider.Projection.TileSize.Width);
#endif
            sizeOfMapArea.Width = 1 + (diag / 2);
            sizeOfMapArea.Height = 1 + (diag / 2);
         }
         else
         {
            sizeOfMapArea.Width = 1 + (Width / Provider.Projection.TileSize.Width) / 2;
            sizeOfMapArea.Height = 1 + (Height / Provider.Projection.TileSize.Height) / 2;
         }

         Debug.WriteLine("OnMapSizeChanged, w: " + width + ", h: " + height + ", size: " + sizeOfMapArea);

         if (IsStarted)
         {
            UpdateBounds();
            GoToCurrentPosition();
         }
      }

      /// <summary>
      /// gets current map view top/left coordinate, width in Lng, height in Lat
      /// </summary>
      /// <returns></returns>
      public RectLatLng ViewArea
      {
         get
         {
            if (Provider.Projection != null)
            {
               var p = FromLocalToLatLng(0, 0);
               var p2 = FromLocalToLatLng(Width, Height);

               return RectLatLng.FromLTRB(p.Lng, p.Lat, p2.Lng, p2.Lat);
            }
            return RectLatLng.Empty;
         }
      }

      /// <summary>
      /// gets lat/lng from local control coordinates
      /// </summary>
      /// <param name="x"></param>
      /// <param name="y"></param>
      /// <returns></returns>
      public PointLatLng FromLocalToLatLng(long x, long y)
      {
         GPoint p = new GPoint(x, y);
         p.OffsetNegative(renderOffset);
         p.Offset(compensationOffset);

         return Provider.Projection.FromPixelToLatLng(p, Zoom);
      }

      /// <summary>
      /// return local coordinates from lat/lng
      /// </summary>
      /// <param name="latlng"></param>
      /// <returns></returns>
      public GPoint FromLatLngToLocal(PointLatLng latlng)
      {
         GPoint pLocal = Provider.Projection.FromLatLngToPixel(latlng, Zoom);
         pLocal.Offset(renderOffset);
         pLocal.OffsetNegative(compensationOffset);
         return pLocal;
      }

      /// <summary>
      /// gets max zoom level to fit rectangle
      /// </summary>
      /// <param name="rect"></param>
      /// <returns></returns>
      public int GetMaxZoomToFitRect(RectLatLng rect)
      {
         int zoom = minZoom;

         if (rect.HeightLat == 0 || rect.WidthLng == 0)
         {
            zoom = maxZoom / 2;
         }
         else
         {
            for (int i = (int)zoom; i <= maxZoom; i++)
            {
               GPoint p1 = Provider.Projection.FromLatLngToPixel(rect.LocationTopLeft, i);
               GPoint p2 = Provider.Projection.FromLatLngToPixel(rect.LocationRightBottom, i);

               if (((p2.X - p1.X) <= Width + 10) && (p2.Y - p1.Y) <= Height + 10)
               {
                  zoom = i;
               }
               else
               {
                  break;
               }
            }
         }

         return zoom;
      }

      /// <summary>
      /// initiates map dragging
      /// </summary>
      /// <param name="pt"></param>
      public void BeginDrag(GPoint pt)
      {
         dragPoint.X = pt.X - renderOffset.X;
         dragPoint.Y = pt.Y - renderOffset.Y;
         IsDragging = true;
      }

      /// <summary>
      /// ends map dragging
      /// </summary>
      public void EndDrag()
      {
         IsDragging = false;
         mouseDown = GPoint.Empty;

         Refresh.Set();
      }

      /// <summary>
      /// reloads map
      /// </summary>
      public void ReloadMap()
      {
         if (IsStarted)
         {
            Debug.WriteLine("------------------");

            okZoom = 0;
            skipOverZoom = 0;

            CancelAsyncTasks();

            Matrix.ClearAllLevels();

            lock (FailedLoads)
            {
               FailedLoads.Clear();
               RaiseEmptyTileError = true;
            }

            Refresh.Set();

            UpdateBounds();
         }
         else
         {
            throw new Exception("Please, do not call ReloadMap before form is loaded, it's useless");
         }
      }

      /// <summary>
      /// moves current position into map center
      /// </summary>
      public void GoToCurrentPosition()
      {
         compensationOffset = positionPixel; // TODO: fix

         // reset stuff
         renderOffset = GPoint.Empty;
         dragPoint = GPoint.Empty;

         //var dd = new GPoint(-(CurrentPositionGPixel.X - Width / 2), -(CurrentPositionGPixel.Y - Height / 2));
         //dd.Offset(compensationOffset);

         var d = new GPoint(Width / 2, Height / 2);

         this.Drag(d);
      }

      public bool MouseWheelZooming = false;

      /// <summary>
      /// moves current position into map center
      /// </summary>
      internal void GoToCurrentPositionOnZoom()
      {
         compensationOffset = positionPixel; // TODO: fix

         // reset stuff
         renderOffset = GPoint.Empty;
         dragPoint = GPoint.Empty;

         // goto location and centering
         if (MouseWheelZooming)
         {
            if (MouseWheelZoomType != MouseWheelZoomType.MousePositionWithoutCenter)
            {
               GPoint pt = new GPoint(-(positionPixel.X - Width / 2), -(positionPixel.Y - Height / 2));
               pt.Offset(compensationOffset);
               renderOffset.X = pt.X - dragPoint.X;
               renderOffset.Y = pt.Y - dragPoint.Y;
            }
            else // without centering
            {
               renderOffset.X = -positionPixel.X - dragPoint.X;
               renderOffset.Y = -positionPixel.Y - dragPoint.Y;
               renderOffset.Offset(mouseLastZoom);
               renderOffset.Offset(compensationOffset);
            }
         }
         else // use current map center
         {
            mouseLastZoom = GPoint.Empty;

            GPoint pt = new GPoint(-(positionPixel.X - Width / 2), -(positionPixel.Y - Height / 2));
            pt.Offset(compensationOffset);
            renderOffset.X = pt.X - dragPoint.X;
            renderOffset.Y = pt.Y - dragPoint.Y;
         }

         UpdateCenterTileXYLocation();
      }

      /// <summary>
      /// darg map by offset in pixels
      /// </summary>
      /// <param name="offset"></param>
      public void DragOffset(GPoint offset)
      {
         renderOffset.Offset(offset);

         UpdateCenterTileXYLocation();

         if (centerTileXYLocation != centerTileXYLocationLast)
         {
            centerTileXYLocationLast = centerTileXYLocation;
            UpdateBounds();
         }

         {
            LastLocationInBounds = Position;

            IsDragging = true;
            Position = FromLocalToLatLng((int)Width / 2, (int)Height / 2);
            IsDragging = false;
         }

         if (OnMapDrag != null)
         {
            OnMapDrag();
         }
      }

      /// <summary>
      /// drag map
      /// </summary>
      /// <param name="pt"></param>
      public void Drag(GPoint pt)
      {
         renderOffset.X = pt.X - dragPoint.X;
         renderOffset.Y = pt.Y - dragPoint.Y;

         UpdateCenterTileXYLocation();

         if (centerTileXYLocation != centerTileXYLocationLast)
         {
            centerTileXYLocationLast = centerTileXYLocation;
            UpdateBounds();
         }

         if (IsDragging)
         {
            LastLocationInBounds = Position;
            Position = FromLocalToLatLng((int)Width / 2, (int)Height / 2);

            if (OnMapDrag != null)
            {
               OnMapDrag();
            }
         }
      }

      /// <summary>
      /// cancels tile loaders and bounds checker
      /// </summary>
      public void CancelAsyncTasks()
      {
         if (IsStarted)
         {
#if NET40
                //TODO: clear loading
#else
            Monitor.Enter(tileLoadQueue);
            try
            {
               tileLoadQueue.Clear();
            }
            finally
            {
               Monitor.Exit(tileLoadQueue);
            }
#endif
         }
      }

      bool RaiseEmptyTileError = false;
      internal Dictionary<LoadTask, Exception> FailedLoads = new Dictionary<LoadTask, Exception>(new LoadTaskComparer());

      internal static readonly int WaitForTileLoadThreadTimeout = 5 * 1000 * 60; // 5 min.

      volatile int okZoom = 0;
      volatile int skipOverZoom = 0;

#if NET40
        static readonly BlockingCollection<LoadTask> tileLoadQueue4 = new BlockingCollection<LoadTask>(new ConcurrentStack<LoadTask>());
        static List<Task> tileLoadQueue4Tasks;
        static int loadWaitCount = 0;
        void AddLoadTask(LoadTask t)
        {
            if (tileLoadQueue4Tasks == null)
            {
                lock (tileLoadQueue4)
                {
                    if (tileLoadQueue4Tasks == null)
                    {
                        tileLoadQueue4Tasks = new List<Task>();

                        while (tileLoadQueue4Tasks.Count < GThreadPoolSize)
                        {
                            Debug.WriteLine("creating ProcessLoadTask: " + tileLoadQueue4Tasks.Count);

                            tileLoadQueue4Tasks.Add(Task.Factory.StartNew(delegate ()
                            {
                                string ctid = "ProcessLoadTask[" + Thread.CurrentThread.ManagedThreadId + "]";
                                Thread.CurrentThread.Name = ctid;

                                Debug.WriteLine(ctid + ": started");
                                do
                                { 
                                    if (tileLoadQueue4.Count == 0)
                                    {
                                        Debug.WriteLine(ctid + ": ready");

                                        if (Interlocked.Increment(ref loadWaitCount) >= GThreadPoolSize)
                                        {
                                            Interlocked.Exchange(ref loadWaitCount, 0);
                                            OnLoadComplete(ctid);
                                        }
                                    }
                                    ProcessLoadTask(tileLoadQueue4.Take(), ctid);
                                }
                                while (!tileLoadQueue4.IsAddingCompleted);

                                Debug.WriteLine(ctid + ": exit");

                            }, TaskCreationOptions.LongRunning));
                        }
                    }
                }
            }
            tileLoadQueue4.Add(t);
        }
#else
      byte loadWaitCount = 0;

      void tileLoadThread()
      {
         LoadTask? task = null;
         bool stop = false;

#if !PocketPC
         Thread ct = Thread.CurrentThread;
         string ctid = "Thread[" + ct.ManagedThreadId + "]";
#else
            int ctid = 0;
#endif
         while (!stop && IsStarted)
         {
            task = null;

            Monitor.Enter(tileLoadQueue);
            try
            {
               while (tileLoadQueue.Count == 0)
               {
                  Debug.WriteLine(ctid + " - Wait " + loadWaitCount + " - " + DateTime.Now.TimeOfDay);

                  if (++loadWaitCount >= GThreadPoolSize)
                  {
                     loadWaitCount = 0;
                     OnLoadComplete(ctid);
                  }

                  if (!IsStarted || false == Monitor.Wait(tileLoadQueue, WaitForTileLoadThreadTimeout, false) || !IsStarted)
                  {
                     stop = true;
                     break;
                  }
               }

               if (IsStarted && !stop || tileLoadQueue.Count > 0)
               {
                  task = tileLoadQueue.Pop();
               }
            }
            finally
            {
               Monitor.Exit(tileLoadQueue);
            }

            if (task.HasValue && IsStarted)
            {
               ProcessLoadTask(task.Value, ctid);
            }
         }

#if !PocketPC
         Monitor.Enter(tileLoadQueue);
         try
         {
            Debug.WriteLine("Quit - " + ct.Name);
            lock (GThreadPool)
            {
               GThreadPool.Remove(ct);
            }
         }
         finally
         {
            Monitor.Exit(tileLoadQueue);
         }
#endif
      }
#endif

      static void ProcessLoadTask(LoadTask task, string ctid)
      {
         try
         {
            #region -- execute --

            var m = task.Core.Matrix.GetTileWithReadLock(task.Zoom, task.Pos);
            if (!m.NotEmpty)
            {
               Debug.WriteLine(ctid + " - try load: " + task);

               Tile t = new Tile(task.Zoom, task.Pos);

               foreach (var tl in task.Core.provider.Overlays)
               {
                  int retry = 0;
                  do
                  {
                     PureImage img = null;
                     Exception ex = null;

                     if (task.Zoom >= task.Core.provider.MinZoom && (!task.Core.provider.MaxZoom.HasValue || task.Zoom <= task.Core.provider.MaxZoom))
                     {
                        if (task.Core.skipOverZoom == 0 || task.Zoom <= task.Core.skipOverZoom)
                        {
                           // tile number inversion(BottomLeft -> TopLeft)
                           if (tl.InvertedAxisY)
                           {
                              img = GMaps.Instance.GetImageFrom(tl, new GPoint(task.Pos.X, task.Core.maxOfTiles.Height - task.Pos.Y), task.Zoom, out ex);
                           }
                           else // ok
                           {
                              img = GMaps.Instance.GetImageFrom(tl, task.Pos, task.Zoom, out ex);
                           }
                        }
                     }

                     if (img != null && ex == null)
                     {
                        if (task.Core.okZoom < task.Zoom)
                        {
                           task.Core.okZoom = task.Zoom;
                           task.Core.skipOverZoom = 0;
                           Debug.WriteLine("skipOverZoom disabled, okZoom: " + task.Core.okZoom);
                        }
                     }
                     else if (ex != null)
                     {
                        if ((task.Core.skipOverZoom != task.Core.okZoom) && (task.Zoom > task.Core.okZoom))
                        {
                           if (ex.Message.Contains("(404) Not Found"))
                           {
                              task.Core.skipOverZoom = task.Core.okZoom;
                              Debug.WriteLine("skipOverZoom enabled: " + task.Core.skipOverZoom);
                           }
                        }
                     }

                     // check for parent tiles if not found
                     if (img == null && task.Core.okZoom > 0 && task.Core.fillEmptyTiles && task.Core.Provider.Projection is MercatorProjection)
                     {
                        int zoomOffset = task.Zoom > task.Core.okZoom ? task.Zoom - task.Core.okZoom : 1;
                        long Ix = 0;
                        GPoint parentTile = GPoint.Empty;

                        while (img == null && zoomOffset < task.Zoom)
                        {
                           Ix = (long)Math.Pow(2, zoomOffset);
                           parentTile = new GMap.NET.GPoint((task.Pos.X / Ix), (task.Pos.Y / Ix));
                           img = GMaps.Instance.GetImageFrom(tl, parentTile, task.Zoom - zoomOffset++, out ex);
                        }

                        if (img != null)
                        {
                           // offsets in quadrant
                           long Xoff = Math.Abs(task.Pos.X - (parentTile.X * Ix));
                           long Yoff = Math.Abs(task.Pos.Y - (parentTile.Y * Ix));

                           img.IsParent = true;
                           img.Ix = Ix;
                           img.Xoff = Xoff;
                           img.Yoff = Yoff;

                           // wpf
                           //var geometry = new RectangleGeometry(new Rect(Core.tileRect.X + 0.6, Core.tileRect.Y + 0.6, Core.tileRect.Width + 0.6, Core.tileRect.Height + 0.6));
                           //var parentImgRect = new Rect(Core.tileRect.X - Core.tileRect.Width * Xoff + 0.6, Core.tileRect.Y - Core.tileRect.Height * Yoff + 0.6, Core.tileRect.Width * Ix + 0.6, Core.tileRect.Height * Ix + 0.6);

                           // gdi+
                           //System.Drawing.Rectangle dst = new System.Drawing.Rectangle((int)Core.tileRect.X, (int)Core.tileRect.Y, (int)Core.tileRect.Width, (int)Core.tileRect.Height);
                           //System.Drawing.RectangleF srcRect = new System.Drawing.RectangleF((float)(Xoff * (img.Img.Width / Ix)), (float)(Yoff * (img.Img.Height / Ix)), (img.Img.Width / Ix), (img.Img.Height / Ix));
                        }
                     }

                     if (img != null)
                     {
                        Debug.WriteLine(ctid + " - tile loaded: " + img.Data.Length / 1024 + "KB, " + task);
                        {
                           t.AddOverlay(img);
                        }
                        break;
                     }
                     else
                     {
                        if (ex != null)
                        {
                           lock (task.Core.FailedLoads)
                           {
                              if (!task.Core.FailedLoads.ContainsKey(task))
                              {
                                 task.Core.FailedLoads.Add(task, ex);

                                 if (task.Core.OnEmptyTileError != null)
                                 {
                                    if (!task.Core.RaiseEmptyTileError)
                                    {
                                       task.Core.RaiseEmptyTileError = true;
                                       task.Core.OnEmptyTileError(task.Zoom, task.Pos);
                                    }
                                 }
                              }
                           }
                        }

                        if (task.Core.RetryLoadTile > 0)
                        {
                           Debug.WriteLine(ctid + " - ProcessLoadTask: " + task + " -> empty tile, retry " + retry);
                           {
                              Thread.Sleep(1111);
                           }
                        }
                     }
                  }
                  while (++retry < task.Core.RetryLoadTile);
               }

               if (t.HasAnyOverlays && task.Core.IsStarted)
               {
                  task.Core.Matrix.SetTile(t);
               }
               else
               {
                  t.Dispose();
               }
            }

            #endregion
         }
         catch (Exception ex)
         {
            Debug.WriteLine(ctid + " - ProcessLoadTask: " + ex.ToString());
         }
         finally
         {
            if (task.Core.Refresh != null)
            {
               task.Core.Refresh.Set();
            }
         }
      }

      void OnLoadComplete(string ctid)
      {
         LastTileLoadEnd = DateTime.Now;
         long lastTileLoadTimeMs = (long)(LastTileLoadEnd - LastTileLoadStart).TotalMilliseconds;

         #region -- clear stuff--
         if (IsStarted)
         {
            GMaps.Instance.MemoryCache.RemoveOverload();

            tileDrawingListLock.AcquireReaderLock();
            try
            {
               Matrix.ClearLevelAndPointsNotIn(Zoom, tileDrawingList);
            }
            finally
            {
               tileDrawingListLock.ReleaseReaderLock();
            }
         }
         #endregion

         UpdateGroundResolution();
#if UseGC
            GC.Collect();
            GC.WaitForPendingFinalizers();
            GC.Collect();
#endif
         Debug.WriteLine(ctid + " - OnTileLoadComplete: " + lastTileLoadTimeMs + "ms, MemoryCacheSize: " + GMaps.Instance.MemoryCache.Size + "MB");

         if (OnTileLoadComplete != null)
         {
            OnTileLoadComplete(lastTileLoadTimeMs);
         }
      }

      public AutoResetEvent Refresh = new AutoResetEvent(false);

      public bool updatingBounds = false;

      /// <summary>
      /// updates map bounds
      /// </summary>
      void UpdateBounds()
      {
         if (!IsStarted || Provider.Equals(EmptyProvider.Instance))
         {
            return;
         }

         updatingBounds = true;

         tileDrawingListLock.AcquireWriterLock();
         try
         {
            #region -- find tiles around --
            tileDrawingList.Clear();
            int countI = (int)Math.Ceiling(sizeOfMapArea.Width * scaleX);
            int countJ = (int)Math.Ceiling(sizeOfMapArea.Height * scaleY);
            int startI = (int)Math.Floor(-sizeOfMapArea.Width * scaleX);
            int startJ = (int)Math.Floor(-sizeOfMapArea.Height * scaleY);

#if ContinuesMap
            //ZXHM:获取当采用180度经线为中心后，当前级别下最小的插入像素点和最大插入像素点
            long minPixelX = tileRect.Width * (maxOfTiles.Width + 1) / 2;
            long maxPixelX = tileRect.Width * maxOfTiles.Width + minPixelX;
#endif

            for (long i = startI; i <= countI; i++)
            {
               for (long j = startJ; j <= countJ; j++)
               {
                  GPoint p = centerTileXYLocation;
                  p.X += i;
                  p.Y += j;
                  GPoint pixel = new GPoint(p.X * tileRect.Width, p.Y * tileRect.Height);
                  
#if ContinuesMap
                  //ZXHM:不在最小插入像素点和最大插入像素点之内的不要
                  if (pixel.X < minPixelX || pixel.X > maxPixelX)
                  {
                     continue;
                  }
                  //ZXHM:连续地图，XY超出之后复原
                  if (p.X < minOfTiles.Width)
                  {
                     p.X += (maxOfTiles.Width + 1);
                  }

                  if (p.X > maxOfTiles.Width)
                  {
                     p.X -= (maxOfTiles.Width + 1);
                  }
#endif
                  if (p.X >= minOfTiles.Width && p.Y >= minOfTiles.Height && p.X <= maxOfTiles.Width && p.Y <= maxOfTiles.Height)
                  {
                     DrawTile dt = new DrawTile()
                     {
                        PosXY = p,
                        PosPixel = pixel,
                        DistanceSqr = (centerTileXYLocation.X - p.X) * (centerTileXYLocation.X - p.X) + (centerTileXYLocation.Y - p.Y) * (centerTileXYLocation.Y - p.Y)
                     };

                     if (!tileDrawingList.Contains(dt))
                     {
                        tileDrawingList.Add(dt);
                     }
                  }
               }
            }

            if (GMaps.Instance.ShuffleTilesOnLoad)
            {
               Stuff.Shuffle<DrawTile>(tileDrawingList);
            }
            else
            {
               tileDrawingList.Sort();
            }
            #endregion
         }
         finally
         {
            tileDrawingListLock.ReleaseWriterLock();
         }

#if NET40
            Interlocked.Exchange(ref loadWaitCount, 0);
#else
         Monitor.Enter(tileLoadQueue);
         try
         {
#endif
            tileDrawingListLock.AcquireReaderLock();
            try
            {
               foreach (DrawTile p in tileDrawingList)
               {
                  LoadTask task = new LoadTask(p.PosXY, Zoom, this);
#if NET40
                    AddLoadTask(task);
#else
                  {
                     if (!tileLoadQueue.Contains(task))
                     {
                        tileLoadQueue.Push(task);
                     }
                  }
#endif
               }
            }
            finally
            {
               tileDrawingListLock.ReleaseReaderLock();
            }

#if !NET40
            #region -- starts loader threads if needed --

            lock (GThreadPool)
            {
               while (GThreadPool.Count < GThreadPoolSize)
               {
                  Thread t = new Thread(new ThreadStart(tileLoadThread));
                  {
                     t.Name = "TileLoader: " + GThreadPool.Count;
                     t.IsBackground = true;
                     t.Priority = ThreadPriority.BelowNormal;
                  }
                  GThreadPool.Add(t);

                  Debug.WriteLine("add " + t.Name + " to GThreadPool");

                  t.Start();
               }
            }
            #endregion
#endif
            {
               LastTileLoadStart = DateTime.Now;
               Debug.WriteLine("OnTileLoadStart - at zoom " + Zoom + ", time: " + LastTileLoadStart.TimeOfDay);
            }
#if !NET40
            loadWaitCount = 0;
            Monitor.PulseAll(tileLoadQueue);
         }
         finally
         {
            Monitor.Exit(tileLoadQueue);
         }
#endif
         updatingBounds = false;

         if (OnTileLoadStart != null)
         {
            OnTileLoadStart();
         }
      }

      /// <summary>
      /// updates ground resolution info
      /// </summary>
      void UpdateGroundResolution()
      {
         double rez = Provider.Projection.GetGroundResolution(Zoom, Position.Lat);
         pxRes100m = (int)(100.0 / rez); // 100 meters
         pxRes1000m = (int)(1000.0 / rez); // 1km  
         pxRes10km = (int)(10000.0 / rez); // 10km
         pxRes100km = (int)(100000.0 / rez); // 100km
         pxRes1000km = (int)(1000000.0 / rez); // 1000km
         pxRes5000km = (int)(5000000.0 / rez); // 5000km
      }

      #region IDisposable Members

      ~Core()
      {
         Dispose(false);
      }

      void Dispose(bool disposing)
      {
         if (IsStarted)
         {
            if (invalidator != null)
            {
               invalidator.CancelAsync();
               invalidator.DoWork -= new DoWorkEventHandler(invalidatorWatch);
               invalidator.Dispose();
               invalidator = null;
            }

            if (Refresh != null)
            {
               Refresh.Set();
               Refresh.Close();
               Refresh = null;
            }

            int x = Interlocked.Decrement(ref instances);
            Debug.WriteLine("OnMapClose: " + x);

            CancelAsyncTasks();
            IsStarted = false;

            if (Matrix != null)
            {
               Matrix.Dispose();
               Matrix = null;
            }

            if (FailedLoads != null)
            {
               lock (FailedLoads)
               {
                  FailedLoads.Clear();
                  RaiseEmptyTileError = false;
               }
               FailedLoads = null;
            }

            tileDrawingListLock.AcquireWriterLock();
            try
            {
               tileDrawingList.Clear();
            }
            finally
            {
               tileDrawingListLock.ReleaseWriterLock();
            }

#if NET40
                //TODO: maybe
#else
            // cancel waiting loaders
            Monitor.Enter(tileLoadQueue);
            try
            {
               Monitor.PulseAll(tileLoadQueue);
            }
            finally
            {
               Monitor.Exit(tileLoadQueue);
            }

            lock (GThreadPool)
            {
#if PocketPC
                Debug.WriteLine("waiting until loaders are stopped...");
                while(GThreadPool.Count > 0)
                {
                    var t = GThreadPool[0];

                    if (t.State != ThreadState.Stopped)
                    {
                        var tr = t.Join(1111);

                        Debug.WriteLine(t.Name + ", " + t.State);

                        if (!tr)
                        {
                            continue;
                        }
                        else
                        {
                            GThreadPool.Remove(t);
                        }
                    }
                    else
                    {
                        GThreadPool.Remove(t);
                    }
                }
                Thread.Sleep(1111);
#endif
            }
#endif

            if (tileDrawingListLock != null)
            {
               tileDrawingListLock.Dispose();
               tileDrawingListLock = null;
               tileDrawingList = null;
            }

            if (x == 0)
            {
#if DEBUG
               GMaps.Instance.CancelTileCaching();
#endif
               GMaps.Instance.noMapInstances = true;
               GMaps.Instance.WaitForCache.Set();
               if (disposing)
               {
                  GMaps.Instance.MemoryCache.Clear();
               }
            }
         }
      }

      public void Dispose()
      {
         Dispose(true);
         GC.SuppressFinalize(this);
      }

      #endregion
   }
}
